Skip to content

feat: devkitd — Rust coordination daemon + harness ports + CLI bundling - #352

Merged
codeaholicguy merged 58 commits into
mainfrom
feature-devkitd-monorepo
Oct 9, 2026
Merged

codeaholicguy merged 58 commits into
mainfrom
feature-devkitd-monorepo

Conversation

@codeaholicguy

@codeaholicguy codeaholicguy commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Moves ai-devkit agent operations from per-command TS discovery to a machine-local Rust daemon (devkitd) — a tmux/dockerd-style architecture where the CLI is a thin client over a Unix socket.

Daemon (rust/crates/devkitd + devkit-core)

  • Unix-socket JSON-RPC at ~/.ai-devkit/daemon.sock; single daemon per machine (socket-ownership probe refuses double-serve)
  • Sole-writer SQLite (daemon.db): snapshot diff ledger + bounded append-only event log; corrupt-db quarantine + retry
  • RPC surface: ping, daemon.status, agent.list, subscribe (replay + live, liveOnly), shutdown
  • Events: agent.appeared / agent.disappeared carrying full attributed agent rows, subscription.lagged
  • tmux-style autostart from clients: spawn lockfile held until serving, log rotation, stale-socket cleanup
  • 2s discovery sweep feeding a prebuilt EnrichedAgent[] cache — agent.list is an O(1) read

Harness port (rust/crates/devkit-harness) — all 10 adapters ported (Claude, Codex, Pi, Gemini, Copilot, Grok, OpenCode, Devin, Kiro, Antigravity) with committed-fixture parity tests against the TS implementations. TS adapters remain authoritative for conversation rendering, session browsing, readiness, and daemon-down fallback.

CLI

  • agent list/detail/console consume daemon APIs; console refreshes via subscribe events with resubscribe-on-death
  • esbuild bundle (dist/bundle/cli.js, code-split lazy chunks) — agent list 550–670ms → 120–190ms, --version ~50ms
  • Local fallback preserved when the daemon is unreachable

Release plumbing

  • Per-platform binary packages @ai-devkit/devkitd-{darwin,linux}-{arm64,x64} resolved via optionalDependencies (esbuild-style; Linux is musl-static covering glibc + Alpine)
  • New publish workflows: publish-devkitd.yml (4-target cargo matrix) and publish-daemon-client.yml (injects platform pins at publish)

Test plan

  • nx run-many -t lint — 8/8 projects
  • nx run-many -t test — all green (incl. 15 devkit-core + 63 devkit-harness + 6 devkitd Rust tests, fixture-parity vs TS)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • Live daemon: single-instance guard verified, agent list ~120–190ms bundled, subscribe live/replay verified end-to-end
  • Platform packaging dry-run: staged @ai-devkit/devkitd-darwin-x64@0.1.0 publishes cleanly; x86_64-apple-darwin cross-build compiles

codeaholicguy and others added 30 commits October 7, 2026 20:56
…ore, event log, discovery, autostart

Rust workspace at rust/ with a single-static-binary daemon per the daemon
brainstorm: unix socket JSON-RPC (0600 + SO_PEERCRED), rusqlite sole-writer
store absorbing the channels/pi-sessions registries, persisted append-only
event log with at-least-once subscribe/replay, and a single ps-sweep discovery
loop feeding agent.list/agent.appeared/agent.disappeared.

TS side: new @ai-devkit/daemon-client package (line JSON-RPC client, tmux-style
flock-guarded autostart, platform-binary resolution via AI_DEVKITD_BIN /
@ai-devkit/daemon-<plat>-<arch> optional dep / dev target dir).
ChannelConfigRepository and the pi-session-tracker extension are daemon-first
with file fallback; CLI gains `ai-devkit daemon status|start|stop|logs|install`.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…pawn-lock

Post-review remediation:
- docs/ai phase docs (requirements/design/planning/implementation/testing)
  honestly reflecting the built v1 and deviations from the brainstorm.
- Rust: proto wire-contract tests, discover golden ps fixture, store
  concurrent-sole-writer + persistence tests, server end-to-end socket test
  (replay + live subscribe, malformed-line resilience).
- daemon-client: autostart + fallback integration tests against the real
  binary; vitest config scoping src tests (dist was double-running).
- Fix real bug found by the new tests: the spawn lockfile was never unlinked,
  so one spawn wedged all future autostarts; holder now unlinks, and locks
  older than 30s are reclaimed.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Console useAgentList now subscribes to daemon events as invalidation
signals: refreshes immediately on agent.appeared/disappeared and
registry.changed events, and relaxes the 3s poll to a 60s fallback
while subscribed. When the daemon is unavailable or subscription
fails, the original 3s polling behavior is preserved.

The subscription wiring lives in agentListSubscription.ts so it can
be unit-tested directly (Ink does not reliably flush useEffect in
tests). ensureDaemon() now also skips autostart under vitest to keep
tests hermetic.

Lifecycle docs rewritten fully self-contained and console migration
folded into requirements/design/planning/testing.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…go into nx

Rename the daemon binary to devkitd and split the workspace into
crates/devkitd (transport/lifecycle) and crates/devkit-core (proto, store,
discovery) so future Rust components — harness parsers, tools — have a
library to build on. devkit-core's proto types now generate the TS wire
types in daemon-client via ts-rs, eliminating hand-mirrored contracts.

Nx gains a rust project (nx:run-commands wrapping cargo build/test/clippy/
fmt) so nx run-many covers both languages in one graph.

Binary resolution: DEVKITD_BIN is primary, AI_DEVKITD_BIN remains a
deprecated alias. Socket/db/log paths are product-scoped and unchanged.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…rness crate, fixture oracle

I0 of the harness-port epic (docs/ai/planning/2026-10-08-epic-harness-port.md):
the substrate every per-harness port iteration builds on. No adapter is
ported yet; agent.enriched returns {agents: [], ported: []} and clients
stay on the local path — zero behavior change.

- devkit-core: EnrichedAgent/EnrichedAgentsResult wire types (serde
  camelCase mirrors AgentInfo exactly) + ts-rs bindings into
  daemon-client/src/gen
- devkit-harness: SweepContext, HarnessAdapter trait, Registry with
  ported_types + enrich — the seam real adapters land on in I1+
- devkitd: enrichment cached per 2s sweep (N clients never multiply parse
  work); agent.enriched serves the cache
- daemon-client: enrichedAgents() + real-socket integration test
- agent-manager/src/fixtures: golden-bundle capture/replay — the parity
  oracle for Rust↔TS equivalence; replay proves byte-identical output on a
  live-captured claude bundle. Live bundles are gitignored (private
  transcripts); committed synthetic fixtures land per-iteration.

Verified: cargo 20 tests, clippy/fmt clean, daemon-client 11, agent-manager
1312 (incl. fixture replay), tsc/oxlint clean.
I1 of the harness-port epic. devkitd now owns claude attribution:
devkit-harness::claude ports the full detect path (resume/PID-file/
legacy matching + bounded head/tail JSONL parser + status/summary
derivation), discover.rs gains batch lsof-cwd + ps-lstart enrichment,
and agent.enriched reports ported=[claude]. AgentManager consumes
daemon rows for ported types via injectable fetchEnrichedAgents with
all-local fallback; cli wires it through ensureDaemon (implicit spawn).

Fixture oracle extended with $NOW sentinel; committed synthetic
bundles replay byte-identical on TS and Rust.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
I2 of the harness-port epic: CodexAdapter detection moves to Rust and
the daemon reports ported: ["claude","codex"].

- codex/mod.rs: helper-subcommand filter with clap-accurate flag
  parsing (variadic --image, -- stop, app-server-daemon exclusion),
  sessions.json mapping, registry cache, locator, process-only
  fallback — TS stage/output order preserved.
- codex/locator.rs: resume <uuid>, uuidv7 day-dir window, session_meta
  head parse with truncated-JSON regex fallback, legacy greedy match,
  30s unmatched negative cache.
- codex/parser.rs: bounded head/tail JSONL fold, session_meta gate,
  idle/waiting/running status, summary extraction.
- shared.rs: birthtime_ms, local_day_key (libc localtime_r), is_idle,
  truncate, parse_timestamp_ms, fold_jsonl_bounded, read_file_head,
  registry_session_paths (readonly ~/.ai-devkit/agents.db).
- Fixtures: bundles gain a `registry` field (seeded isolated agents.db
  on both replay sides); $TODAY normalization scoped to $NOW-start
  pids so live bundles replay on any day; live capture snapshots the
  registry pre-detect and includes .codex/ai-devkit.
- Parity: synthetic matched/statuses bundles + live bundle (12 real
  agents) byte-identical on TS and Rust replay.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Third harness ported to Rust: daemon now reports ported
["claude","codex","pi"]. Thin consoles get identical enriched pi
state from agent.enriched; AgentManager keeps local fallback for
unported types.

- pi/mod.rs: token-scan executable detection (pi/pi.js/pi.exe),
  sessions.json tracker with trusted-path gate, registry fallback
  (type='pi' readonly SQLite), detect pipeline in TS order
- pi/locator.rs: encoded project dirs under ~/.pi/agent/sessions,
  filename-timestamp or birthtime candidacy, greedy <=3min match
- pi/parser.rs: bounded head/tail fold, first-entry metadata sticky,
  filename session-id fallback, entry-free files -> None
- shared.rs: fix is_idle float-division parity (ms > 300000)
- capture: .pi/agent/sessions + sessions.json; pi REGISTRY_AWARE
- fixtures: matched.json (tracker/registry/legacy/process-only/skip)
  + fallback.json (stale-type row, untrusted path, entry-free file)
- live bundle replayed byte-identical TS/Rust; daemon smoke verified
  real pi pid 28617 enriched via agent.enriched

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- GeminiCliAdapter in devkit-harness: node-pool process filter,
  executable token scan (gemini/gemini.exe/gemini.js), sha256
  project-hash + .project_root slug session discovery, .jsonl log
  replay (upsert/$set/$rewindTo) and legacy .json docs, wrapper
  registry-name override, registry cache, process-only fallback
- daemon sweep now includes node/bun runtime argv0s so node-script
  harnesses reach adapters; visible pool gated on any_can_handle
  (mirrors isCandidateProcess) — also fixes pi-as-node-script gap
- shared: wrapper-pid helpers + registry_agent_rows
- fixtures: matched/fallback synthetic bundles + live bundle; atomic
  counter in materialize_home fixes parallel replay db collision

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- CopilotAdapter: inuse.<pid>.lock session attribution under
  ~/.copilot/session-state with dir-mtime bounded scan and the
  (pid,startTime) known-lock cache; events.jsonl bounded fold
  (session.start overrides, user/assistant text) + naive workspace.yaml
  fallback; wrapper registry-name override; process-only fallback
- fixtures: matched/fallback/workspace synthetic bundles replay
  byte-identical TS vs Rust
- TS capture: .copilot/session-state index dir; registry-aware replay

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- GrokAdapter: active_sessions.json pid->cwd resolution, latest-mtime
  session dir under sessions/<encodeURIComponent(cwd)>/, bounded
  chat_history.jsonl fold (<user_query> extraction, context injections
  skipped), idle/waiting/running from mtime + last role, process-only
- shared: flatten_text_blocks, encode_uri_component (JS unreserved set)
- fixture bundles gain a mtimes map (epoch ms | $NOW) applied at
  materialization — mtime-derived output (Grok lastActive, latest-
  session pick) replays deterministically; live capture records real
  file mtimes
- fixtures: matched/fallback synthetic bundles, byte-identical both sides

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
SQLite-backed session attribution: readonly opencode.db lookup by proc
cwd, session stats from message/part queries (heartbeat, completion,
first user text), process-only fallback. Adds a 'sqlite' bundle section
(SQL statements materialized on both replay sides) and pins
XDG_DATA_HOME so replay never sees the real store. Fixes a latent
processOnlyAgent crash on cwd-less processes (cwd ?? "") in shared.ts.
Lock-first session attribution: session_locks slugs map holder pid +
ppid to session ids (devin acp backends excluded as agents), with
sessions.working_directory fallback in sessions.db. Stats from
message_nodes frontier/heartbeat and two-tier prompt summary. Bounded
sqlite dump captures exactly the rows detection can reach; fixture
replay accepts float mtimes.
Session locks under ~/.kiro/sessions/cli map holder pids to sessions via
ppid-walk to the outermost kiro process (or sole-kiro-on-tty fallback);
sibling .json/.jsonl supply metadata and a bounded transcript summary.

Adds shared::list_dir_names — Node >=20.1 readdirSync returns sorted
entries while raw read_dir does not; every safeReaddir mirror site now
sorts (kiro's lock-ordered output exposed the divergence).
cwd-keyed last_conversations.json registry joins agy procs to their
brain/<id> transcript; the bounded fold keeps MAX created_at, the last
user/assistant role, and the USER_REQUEST prompt. Also fixes
matches_executable_name to normalize .exe on candidate basenames (TS
does both sides), and moves capture's index-dir rule to an explicit
HARNESS_INDEX_DIRS so the 31MB brain tree stays referenced-only.

All ten harnesses are now ported: claude, codex, pi, gemini_cli,
copilot, grok_cli, opencode, devin, kiro, antigravity_cli.
The readiness checks + per-harness profiles now run inside the daemon:
agent.readiness serves reports for all ten types (executable/config/
builtin-skills checks plus the claude/codex/copilot/devin/opencode/pi
auth and integration probes). The TS injectable seams become a Host
trait; fixtures/readiness bundles replay byte-identical between the
TS oracle and a Rust FixtureHost. status.service is daemon-primary
when no function seams are injected, with the local path as fallback.
All ten harness types are ported, so listAgents does zero local
detection whenever devkitd answers — the TS adapters are now fallback
only. channel-runner was the last consumer building a daemon-less
AgentManager; it now wires fetchEnrichedAgents like commands/agent.ts.

Metrics: 6,845 Rust LOC vs 9,850 TS across the ten adapters; 19
committed harness bundles + 2 readiness bundles replayed
byte-identical; agent.enriched answers in 0.4ms warm vs ~238ms for a
local sweep; devkitd idles at ~74MB RSS, ~7% CPU per 2s sweep.
Startup was dominated by module loading for commands that never ran:
ink/react/ConsoleApp (~300ms), the init install stack (~330ms), and
@ai-devkit/memory (~137ms) all loaded eagerly, plus every installed
plugin entrypoint was imported on each invocation (~160ms).

- cli.ts becomes a bootstrap: enables the V8 compile cache (Node >=22.8)
  before loading main.js, which keeps the program body unchanged.
- init, memory, agent.service, @inquirer/prompts, ink/react/ConsoleApp,
  and session-compact are now imported inside the actions that need them.
- Plugin registration is skipped when argv[2] is a built-in command or
  an early-exit flag; help/bare/plugin invocations still scan. Moved
  BUILT_IN_COMMAND_NAMES to a leaf module and added the missing builtins
  (setup, capacity, status, daemon).
- devkitd: the enriched write lock was held for the entire sweep
  (*w = f() evaluates the guard first), stalling agent.enriched for the
  full ~800ms debug sweep. Compute outside the lock and run sweeps on
  spawn_blocking so they never occupy the async runtime.
- vitest: pin AI_DEVKIT_NO_DAEMON in cli setup so a live developer
  daemon cannot stream events into subscription-based tests.

agent list warm: ~630ms with 1.4-2.4s spikes -> ~290ms median.
ChannelConfigRepository cached a persistent DaemonClient whose socket was
never closed. In the agent console the connection outlived unmount and
pinned the event loop, so 'q' left the process running until Ctrl-C;
one-shot commands like 'channel list' hung the same way. Scope each
registry call to a connect/request/close pair.
readiness_reports ran all harness checks serially while spawning probes
like 'opencode auth list' (~5s timeout) and 'pi list' (~4s) — the RPC
took ~4.2s vs the TS path's Promise.all which finishes in ~max probe.
Also dispatch ran inline on the async connection task, so a slow probe
occupied a runtime worker.

Checks and harnesses now fan out on scoped threads (Host gains a Sync
bound), dispatch runs on spawn_blocking, and a 30s params-keyed cache
serves warm 'status' calls in ~1ms instead of re-probing.
channels.json owns channel credentials; the daemon registry path is
removed from ChannelConfigRepository along with its per-RPC client
scoping — no daemon client means no socket to leak. The registry table
and RPCs remain as generic infra for future non-secret state.
codeaholicguy and others added 28 commits October 9, 2026 06:52
channel-bridges.json held live bridge process state — non-secret,
multi-writer data with a read-modify-write race (getLiveBridges rewrote
the file on every read, so a concurrent channel list could drop a
freshly-registered bridge and orphan the process). The daemon registry
scope "channel-bridges" replaces it: each mutation is an atomic
registry.put/delete keyed by channel name.

- getLiveBridges is now a pure read+pid-filter; dead entries prune on
  unregister instead of on every read
- daemon-primary with file fallback; custom registryPath still selects
  file semantics for tests
- listBridges unions file entries not present in the scope so a bridge
  registered under the old scheme stays visible during transition
seq is assigned under the store mutex but the broadcast send happened
after emit() returned — two racing emits could deliver seq N+1 before
seq N. A std mutex now guards store.emit + events.send together so
subscribers observe events in seq order; required for seq-based dedup
in the subscribe path.
The receiver was registered after the persisted replay finished, so
events emitted in between were silently dropped. Subscribe first, then
replay: overlapping events buffer in the receiver and are filtered by
a per-connection delivery floor (seq > last_delivered), giving
at-least-once delivery with no gap and no duplicates.
sweep() couldn't distinguish a failed ps from an empty process table —
a transient failure applied an empty snapshot, deleting every agent row
and storming disappeared/appeared events on the next good sweep. sweep
now returns Option; apply_sweep keeps the previous snapshot on failure.
ps/lsof had no timeout — a wedged child would pin a blocking-pool thread
and silently freeze discovery forever (stale enriched cache, dead event
stream). run_with_timeout SIGKILLs the child so no zombie is left; a
timed-out ps reads as a failed sweep and keeps the previous snapshot.
known.contains(p)/current.contains(p) made the per-sweep diff O(n^2).
HashSet difference with sorted output keeps deterministic event order.
Two store fixes:
- Agent identity is now (pid, start_time_ms): a recycled pid previously
  updated the dead process's row in place — no disappear/appear events,
  stale first_seen. Deletes stale rows before insert so first_seen
  resets; emits disappeared before appeared; adds column migration and
  a reuse regression test.
- Events table bounded to the newest 10k rows; every 512 emits prunes
  below seq-10000 and checkpoints the WAL (was 4MB after a day),
  amortised off the insert hot path.
- Lagged broadcast subscribers get a subscription.lagged frame (seq 0)
  instead of silently missing history; clients refetch on it
- Warn on previously silent failures: emit errors, snapshot apply
  errors, sweep panics, replay errors, abnormal conn close
- Cheap RPCs (ping/daemon.status/agent.enriched) dispatch inline
  instead of paying a spawn_blocking pool hop
- events.replay limit clamps to u32 instead of wrapping
- Request bytes per connection capped at 4MiB
- Corrupt daemon.db quarantined (with wal/shm) and recreated once
  instead of crash-looping devkitd on every autostart
- run_command probes resolve against the client-supplied PATH —
  previously used the daemon's ambient PATH, which could report
  executable: ready while auth probes failed to find the binary
- devkitd status probes the socket (listening / stale / not running)
  and install is gated to Linux (systemd unit is dead weight elsewhere)
- data dir tightened to 0700 so wal/shm sidecars aren't world-readable
- Pending requests now reject when the socket dies (was: awaited
  forever) and carry a 30s default timeout
- Persistent error/close listeners replace a consumable once("error")
  that could leave later errors unhandled and crash the process
- close() drains pending instead of orphaning them
- New onDisconnect hook for callers that need to resubscribe
- Spawn lock now held until the socket answers — releasing right after
  spawn() let a second caller spawn a twin devkitd that clobbered the
  socket path and orphaned the first daemon
- daemon.log rotates to .1 past 1 MiB (was unbounded append)
- Dev binary resolution picks the freshest profile by mtime and uses
  fileURLToPath instead of URL.pathname
The call-site gate in ab53abc left the fn unused on macOS, failing
clippy -D warnings.
- subscribe accepts liveOnly — the console is a live UI; replaying the
  persisted event log (up to 10k frames) only triggered redundant
  refreshes
- When the subscription socket dies (daemon restart) the console now
  resumes fast polling and retries attachDaemonRefresh after 2s via
  the client's onDisconnect hook, instead of silently staying on the
  60s fallback forever
Now that all ten harness types are ported, the raw agent.list RPC and the
ported[] coverage field carry no information: a successful agent.enriched
answer is authoritative for every type.

- agent.enriched returns a bare EnrichedAgent[] (no wrapper)
- agent.list RPC removed; the agents table stays an internal diff ledger
- AgentManager skips all local adapters when the daemon answers
- regenerate bindings, update tests and docs
The qualified name only existed to distinguish the attributed view from
the raw-snapshot RPC that no longer exists — one list API, one name.

- wire method agent.enriched -> agent.list (dispatch, INLINE_METHODS)
- DaemonClient.enrichedAgents -> listAgents; call sites updated
- tests and design doc updated
agent.appeared/disappeared payloads were bare {pid}, forcing every
consumer to refetch to learn what actually changed. Payloads are now
{pid, agents: EnrichedAgent[]} — appeared resolves against the fresh
sweep cache, disappeared against the previous one (the row is already
gone from the new list, so this preserves last-known info). Unattributed
procs emit an empty agents array.
- events.replay RPC: no callers; subscribe's internal replay covers the
  at-least-once path
- registry.changed: emitted on put/delete but nothing consumed it
  semantically — the console refreshes on any frame and registry writes
  don't affect the agent list

Remaining kinds: agent.appeared/disappeared (console refresh signal) and
subscription.lagged (subscriber plumbing).
The scoped registry's only consumer (channel-bridge bookkeeping) already
kept a complete JSON-file path, and daemon rows had to be unioned with
file rows at read time — two overlapping stores worse than one.

- channel.service drops the daemon branch; channel-bridges.json is again
  the single store (mergeFileBridges/pruneDeadBridges/client() gone)
- server drops registry.get/put/delete
- store drops the registry table + methods; open() DROPs it for existing
  databases; concurrency test rewritten over emit/events_after
- channel.service.daemon.test.ts removed with the path it covered
…iness

Status probing is a rare, human-paced command — the daemon's only real
win was a 30s cross-client cache, not worth the whole ported subsystem.

- daemon: remove agent.readiness RPC, readiness cache, devkit-harness
  readiness module, devkit-core readiness types, gen bindings
- client: status.service calls getAgentReadinessReports directly (local
  probes, seams preserved); DaemonClient.agentReadiness removed
- daemon surface is now just what the daemon is for: agent.list,
  subscribe, lifecycle events
agent list was ~300-600ms of which ~85% was Node module resolution —
~200 files across cli + agent-manager + deps. The daemon made the work
nearly free; boot was the floor.

- esbuild bundle of src/cli.ts -> dist/bundle/cli.js (ESM, splitting):
  eager graph collapses to one ~285KB chunk; TUI/ink, init, session-
  compact stay as lazy chunks behind their existing dynamic imports
- externals: better-sqlite3, yoga-layout (native/wasm), @ai-devkit/
  memory (lazy + onnxruntime wasm), react-devtools-core (ink optional)
- createRequire banner for CJS deps that call bare require() (telegraf)
- copy agent-manager SQL migrations beside the bundle (resolved via
  dirname(import.meta.url)/migrations)
- bin -> dist/bundle/cli.js; swc dist tree kept for library consumers

agent list 550ms -> ~120-190ms; --version 240ms -> ~50ms
- drop unused regex/anyhow from devkit-harness deps
- Store::list_agents is test-only since agent.list moved to the cache
- daemon-client: remove dead connectDaemon alias + unused gen re-exports
- delete harness-readiness lifecycle docs (feature removed this branch)
- fix stale agent.enriched/ported references in kept port docs
- fix stale comment in readiness-fixtures test (Rust side deleted)
registry.changed events were removed with the registry RPCs; the
subscription test now exercises agent.disappeared instead.
serve() unconditionally unlinked the socket file before binding, so a
second `devkitd serve` stole the path while the original kept sweeping
and writing daemon.db invisibly. Probe the socket first: connectable
means bail, refused means stale file — safe to unlink and rebind.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
resolveDaemonBinary already looked for @ai-devkit/devkitd-<plat>-<arch>,
but nothing built or published those packages — installs got the
fallback path and the daemon was dead code for end users. daemon-client
itself also had no publish workflow despite being a hard cli dep.

- npm/devkitd-{darwin,linux}-{arm64,x64}: os/cpu-gated package stubs,
  version stamped from daemon-client at publish time
- publish-devkitd.yml: 4-target matrix; linux built musl-static so one
  binary covers glibc and Alpine; darwin x64 cross-compiled on arm64
- publish-daemon-client.yml: injects optionalDependencies pins at
  publish time so dev installs never resolve unpublished packages

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Conflicts resolved:
- agent.ts deleted upstream (split into commands/agent/*.command.ts);
  daemon fetchEnrichedAgents wiring moved to factory.ts
- cli.ts: kept the thin shim + compile cache; upstream program body
  already lives in main.ts — only the agent/index.js path updated
- package.json: upstream 0.68.0 deps + daemon-client kept

The upstream split eagerly pulled ink/react, telegraf and the TypeSafe
SDK (undici) into the bundle's main chunk (285KB -> 4MB, --version
50ms -> 210ms). Re-lazified inside the action bodies: TUI via
console.command.ts, session-compact classifier via session.command.ts,
Telegraf via channel.ts. main chunk back to 289KB.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
fixtures/ was created during the harness port but never tracked — the
parity tests assert at least one bundle, so CI's clean checkout fails
rust:test without them. live*.json captures stay local per .gitignore.
The merge commit b447d45 was built from a mangled index — 173 files
(the rust workspace, daemon-client, npm platform stubs, publish
workflows, fixtures, lifecycle docs) never made it into the tree
despite being on disk. Restores them verbatim; worktree content was
never affected.
Several earlier commits were built from a swept index — their trees
recorded stale blobs or skipped entries, so daemon-primary wiring,
bundling metadata, subscription hook, the pi daemon-first store and
merge resolutions never reached the pushed branch though the code ran
locally. This commits the accumulated worktree state, which always
held the true content.

Includes the user's pending edits: status fixture seam passthrough and
channel-runner's resolveAgentByName adoption.
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@codeaholicguy
codeaholicguy merged commit 224e949 into main Oct 9, 2026
7 checks passed
@codeaholicguy
codeaholicguy deleted the feature-devkitd-monorepo branch October 9, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant